package cz.muni.fi.pb138.avradiation.visualisation.map;

import cz.muni.fi.pb138.avradiation.Sensor.Sensor;
import java.awt.Color;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.geom.AffineTransform;
import java.awt.geom.Line2D;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.math.BigDecimal;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.SortedMap;
import java.util.TreeMap;

/**
 * Class for creation of image map from measured values at one sensor. 
 * Image is created as buffered image.
 * 
 * @author Andrej Kuročenko <kurochenko@gmail.com>
 */
public class ImageMap {

    /** Count of hours in one day */
    private static final int HOURS_IN_DAY = 24;
    /** Count of minutes in one hour */
    private static final int MINUTES_IN_HOUR = 60;
    /** Count of milliseconds in one second */
    private static final int MILLIS_IN_SECOND = 1000;
    /** Minimal image size */
    public static final int MIN_SIZE = 300;
    /** Padding of image borders */
    private static final int PAD = 35;
    /** Width of component with color scale including margins */
    private static double scaleComponentWidth;
    /** Width of component with color */
    private static final int SCALE_WIDTH = 10;
    /** Width of margin of component with color scale */
    private static final int SCALE_MARGIN = 10;
    /** size of value boundary */
    private static final int BOUNDARY_SIZE = 3;
    /** Color representing minimal measured value */
    private static final Color minColor = Color.black;
    /** Color of maximal measured value */
    private static final Color maxColor = Color.green;
    /** Image map background color */
    private static Color bgColor = Color.LIGHT_GRAY;
    /** Image map foreground color */
    private static Color fgColor = Color.BLACK;
    /** Size of image map */
    private static Integer size = null;
    /** Computed width of image map. Is equal to or lower then 
     * <code>size</code> */
    private static Integer width = null;
    /** Computed height of image map. Is equal to or lower then 
     * <code>size</code> */
    private static Integer height = null;
    /** Computed length of axis and rendering of measured values */
    private static double axisLength;
    /** Maximal measured value */
    private static BigDecimal maxMeasuredValue = BigDecimal.ZERO;
    /** Count of measured values occurrence at one time at one row */
    private static double valuesPerRow;
    /** Measure interval */
    private static int interval = Integer.MIN_VALUE;
    /** Length of one side of square which represent one measured value */
    private static double pixelSize = 0;
    
    /** Format of date output */
    private static final SimpleDateFormat dateFormat 
            = new SimpleDateFormat("MM/dd/yyyy HH:mm:ss");
    /** Font for axis labels and values numbers */
    private static final Font normalFont 
            = new Font(Font.MONOSPACED, Font.PLAIN, 10);
    /** Font for title */
    private static final Font titleFont 
            = new Font(Font.MONOSPACED, Font.BOLD, 10);
    
    

    private ImageMap() {
    }

    /**
     * Static factory method for creating image with default 
     * <code>ImageMap.MIN_SIZE</code> size.
     * 
     * @param sensor sensor with measured values
     * @return image as BufferedImage
     */
    public static BufferedImage createImage(Sensor sensor) {
        if (sensor == null) {
            throw new IllegalArgumentException("Sensor is null");
        }
        setSize(ImageMap.MIN_SIZE);
        return getMap(sensor);
    }

    /**
     * Static factory method for creating image with given size
     * 
     * @param size size of image. has to be greater then 
     * <code>ImageMap.MIN_SIZE</code> 
     * @param sensor sensor with measured values
     * @return image as BufferedImage
     */
    public static BufferedImage createImage(int size, Sensor sensor) {
        if (sensor == null) {
            throw new IllegalArgumentException("Sensor is null");
        }
        if (size < ImageMap.MIN_SIZE) {
            throw new IllegalArgumentException("Passed size is lower then "
                    + "minimal allowed image size.");
        }
        setSize(size);
        return getMap(sensor);
    }

    /**
     * Sets maximal size of image map. Height and width are computed according 
     * this entry. Height and width will be equal to or lower then this entry.
     * 
     * @param size maximal size of image map
     */
    private static void setSize(Integer size) {
        if (size == null) {
            throw new IllegalArgumentException("Size is null");
        }
        if (size < MIN_SIZE) {
            throw new IllegalArgumentException("Size is smaller then minimal "
                    + "allowed size: " + MIN_SIZE);
        }

        ImageMap.size = size;
    }

    /**
     * Creates image as BufferedImage with image map according to measured 
     * values in Sensor
     * 
     * @param sensor sensor with measured values
     * @return image as BufferedImage 
     */
    private static BufferedImage getMap(Sensor sensor) {
        if (sensor == null) {
            throw new IllegalArgumentException("Sensor is null");
        }
        if (sensor.getMeasuredValues() == null) {
            throw new IllegalArgumentException("Measured values are null");
        }
        if (size == null) {
            throw new IllegalStateException("Maximal size of image wasn't set "
                    + "(null).");
        }

        double x;
        double y;

        SortedMap<Long, BigDecimal> values =
                new TreeMap<Long, BigDecimal>(sensor.getMeasuredValues());

        countMetadata(values);

        BufferedImage buffer = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);

        Graphics2D g2 = buffer.createGraphics();
        g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
        g2.setColor(bgColor);
        g2.fillRect(0, 0, width, height);
        FontMetrics fm = g2.getFontMetrics(normalFont);
        g2.setFont(normalFont);

        /* Axis */
        g2.setColor(fgColor);
        g2.draw(new Line2D.Double(PAD, PAD, PAD, height - PAD));
        g2.draw(new Line2D.Double(PAD, height - PAD, PAD + axisLength, height - PAD));

        /* Data */
        int counter = 0;
        for (Long key : values.keySet()) {
            g2.setPaint(getPixelColor(getRatio(values.get(key))));

            x = PAD + (counter % valuesPerRow) * pixelSize;
            y = height - PAD - pixelSize - ((int) (counter / valuesPerRow) * pixelSize);
            g2.fill(new Rectangle2D.Double(x, y, pixelSize, pixelSize));
            counter++;
        }

        /* Scale */
        for (int i = 0; i < valuesPerRow; i++) {
            g2.setPaint(getPixelColor((float) i / (float) valuesPerRow));
            double hgt = (double) (height - PAD - pixelSize - ((i % valuesPerRow) * pixelSize));
            g2.fill(new Rectangle2D.Double(PAD + axisLength + SCALE_MARGIN, hgt, SCALE_WIDTH, pixelSize));
        }

        g2.setColor(fgColor);
        for (int i = 0; i <= valuesPerRow; i++) {
            x = PAD + i * pixelSize;
            y = height - PAD;
            g2.draw(new Line2D.Double(x, y, x, y + BOUNDARY_SIZE));
            if (i % 4 == 0) {
                x -= fm.stringWidth(String.valueOf(i/getMeasuresPerHour()))/2;
                g2.drawString(String.valueOf(i / getMeasuresPerHour()), (float) (x), (float) (y + 15));
            }

            x = PAD;
            y = height - PAD - i * pixelSize;
            g2.draw(new Line2D.Double(x - BOUNDARY_SIZE, y, x, y));
            if (i % 3 == 0) {
                x -= fm.stringWidth(String.valueOf(i)) + BOUNDARY_SIZE;
                g2.drawString(String.valueOf(i), (float) (x), (float) (y + 4));
            }
        }

        String label;
        label = "begin: " + dateFormat.format(new Date(values.firstKey()));
        x = 10;
        y = (15);
        g2.drawString(label, (float) x, (float) y);

        label = "end  : " + dateFormat.format(new Date(values.lastKey()));
        x = 10;
        y = (25);
        g2.drawString(label, (float) x, (float) y);


        /* Axis labels */
        label = "hour";
        x = ((width - fm.stringWidth(label)) / 2);
        y = (height - 5);
        g2.drawString(label, (float) x, (float) y);

        label = "day";
        y = ((height - fm.stringWidth(label)) / 2);
        AffineTransform at = g2.getTransform();
        g2.rotate(270.0 * Math.PI / 180.0, 10, y);
        g2.drawString(label, (float) (5), (float) y);
        g2.setTransform(at);

        fm = g2.getFontMetrics(titleFont);
        g2.setFont(titleFont);

        label = sensor.getName() + " [" + sensor.getUnit() + "]";
        x = ((width - fm.stringWidth(label)) / 2);
        y = (15);
        g2.drawString(label, (float) x, (float) y);

        return buffer;
    }

    /**
     * Computes required values for image rendering from measured values and 
     * maximal image size, such as maximal measured value, measure time interval,
     * size of rendered pixels etc.
     * 
     * @param values measured values
     */
    private static void countMetadata(SortedMap<Long, BigDecimal> values) {
        if (values == null) {
            throw new IllegalArgumentException("Measured values are null");
        }

        Long prevTime = null;
        Integer actualInterval = Integer.MIN_VALUE;

        for (Long time : values.keySet()) {
            if (values.get(time) != null) {
                maxMeasuredValue = (values.get(time).compareTo(maxMeasuredValue) > 0)
                        ? values.get(time)
                        : maxMeasuredValue;
            }

            if (prevTime != null) {
                actualInterval = measurementInterval(prevTime, time);

                interval = (interval == Integer.MIN_VALUE)
                        ? actualInterval
                        : interval;

                if (actualInterval != interval) {
                    throw new IllegalStateException("Different measure "
                            + "time iterval");
                }
            }
            prevTime = time;
        }

        scaleComponentWidth = SCALE_MARGIN + SCALE_WIDTH;
        ImageMap.width = size;
        ImageMap.height = (int) (size - scaleComponentWidth);

        valuesPerRow = (HOURS_IN_DAY * MINUTES_IN_HOUR) / interval;
        axisLength = height - 2 * PAD;
        pixelSize = (axisLength / valuesPerRow);
    }

    /**
     * Counts time interval in seconds between two times <code>time1</code> 
     * and <code>time2</code> which are millisecond UNIX time
     * 
     * @param time1
     * @param time2
     * @return time interval in seconds
     */
    private static int measurementInterval(long time1, long time2) {
        if (time1 >= time2) {
            throw new IllegalArgumentException("Wrong time to count interval");
        }
        return (int) ((time2 - time1) / (MINUTES_IN_HOUR * MILLIS_IN_SECOND));
    }

    /**
     * Counts ratio of actual value <code>num</code> and maximal 
     * measured value
     * 
     * @param num actual measured value
     * @return ratio 
     */
    private static float getRatio(BigDecimal num) {        
        num =  (num == null) ? BigDecimal.ONE : num;
        
        return num.floatValue() / maxMeasuredValue.floatValue();
    }
    
    /**
     * Counts how many measures sensor does per hour
     * 
     * @return 1 if <code>interval</code> is 0 else measures per hour 
     */
    private static int getMeasuresPerHour() {
        int measures = (interval == 0) ? 1 : MINUTES_IN_HOUR / interval;
        return measures == 0 ? 1  : measures;
    }

    /**
     * Computes color according to  ratio of actual measured value to 
     * maximal measured value.
     * 
     * @param ratio ratio of actual measured value and maximal measured value
     * @return color
     */
    private static Color getPixelColor(float ratio) {
        return new Color(
                (int) (maxColor.getRed() * ratio + minColor.getRed() * (1 - ratio)),
                (int) (maxColor.getGreen() * ratio + minColor.getGreen() * (1 - ratio)),
                (int) (maxColor.getBlue() * ratio + minColor.getBlue() * (1 - ratio)));
    }
}
